//! # Configuration Management
//!
//! This module defines the configuration structures and functions to manage and
//! load configuration settings for the application.
use std::path::{Path, PathBuf};

use fs_err as fs;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::info;

use crate::{environment::Environment, logger, Error, Result};

lazy_static! {
    static ref DEFAULT_FOLDER: PathBuf = PathBuf::from("config");
}

/// Represents the main application configuration structure.
///
/// This struct encapsulates various configuration settings. The configuration
/// can be customized through YAML files for different environments.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
    pub logger: Logger,
    pub server: Server,
    #[cfg(feature = "with-db")]
    pub database: Database,
    pub redis: Option<Redis>,
    pub auth: Option<Auth>,
    #[serde(default)]
    pub workers: Workers,
    pub mailer: Option<Mailer>,

    #[serde(default)]
    pub settings: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Logger {
    pub enable: bool,
    #[serde(default)]
    pub pretty_backtrace: bool,
    pub level: logger::LogLevel,
    pub format: logger::Format,
    pub override_filter: Option<String>,
}

/// Represents the worker mode configuration.
///
/// The `WorkerMode` enum specifies the worker mode, which can be one of
/// `BackgroundQueue`, `ForegroundBlocking`, or `BackgroundAsync`.
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
pub enum WorkerMode {
    #[default]
    /// Workers operate asynchronously in the background, processing queued
    /// tasks.
    BackgroundQueue,
    /// Workers operate in the foreground and block until tasks are completed.
    ForegroundBlocking,
    /// Workers operate asynchronously in the background, processing tasks with
    /// async capabilities.
    BackgroundAsync,
}

/// Represents the database configuration structure.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct Database {
    /// The URI for connecting to the database. For example:
    /// postgres://root:12341234@localhost:5432/rr_app
    pub uri: String,
    /// Enable SQLx statement logging
    pub enable_logging: bool,
    /// Minimum number of connections for a pool
    pub min_connections: u32,
    /// Maximum number of connections for a pool
    pub max_connections: u32,
    /// Set the timeout duration when acquiring a connection
    pub connect_timeout: u64,
    /// Set the idle duration before closing a connection
    pub idle_timeout: u64,
    #[serde(default)]
    /// Run migration up when application loaded
    pub auto_migrate: bool,
    #[serde(default)]
    /// Truncate database when application loaded
    pub dangerously_truncate: bool,
    #[serde(default)]
    /// Recreating schema
    pub dangerously_recreate: bool,
}

/// Represents the Redis configuration structure.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Redis {
    /// The URI for connecting to the Redis server. For example:
    /// redis://127.0.0.1/
    pub uri: String,
    #[serde(default)]
    /// Flush redis when application loaded
    pub dangerously_flush: bool,
}

/// Represents the user authentication configuration structure.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Auth {
    /// JWT authentication
    pub jwt: Option<JWT>,
}

/// Represents JWT configuration structure.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JWT {
    /// The secret key For JWT token
    pub secret: String,
    /// The expiration time for authentication tokens.
    pub expiration: u64,
}

/// Represents the server configuration structure.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Server {
    /// The port on which the server should listen for incoming connections.
    pub port: i32,
    /// The webserver host
    pub host: String,
    /// Identify via the `Server` header
    pub ident: Option<String>,
    /// Middleware configurations for the server, including payload limits,
    /// logging, and error handling.
    pub middlewares: Middlewares,
}

/// Represents the workers configuration structure.
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Workers {
    pub mode: WorkerMode,
    pub queues: Option<Vec<String>>,
}

/// Represents the middleware configuration structure.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Middlewares {
    /// Middleware that enable compression for the response.
    pub compression: Option<EnableMiddleware>,
    /// Middleware that enable etag cache headers.
    pub etag: Option<EnableMiddleware>,
    /// Middleware that limit the payload request.
    pub limit_payload: Option<LimitPayloadMiddleware>,
    /// Middleware that improve the tracing logger and adding trace id for each
    /// request.
    pub logger: Option<EnableMiddleware>,
    /// catch any code panic and log the error.
    pub catch_panic: Option<EnableMiddleware>,
    /// Setting a global timeout for the requests
    pub timeout_request: Option<TimeoutRequestMiddleware>,
    /// Setting cors configuration
    pub cors: Option<CorsMiddleware>,
    /// Serving static assets
    #[serde(rename = "static")]
    pub static_assets: Option<StaticAssetsMiddleware>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct StaticAssetsMiddleware {
    pub enable: bool,
    pub must_exist: bool,
    pub folder: FolderAssetsMiddleware,
    pub fallback: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FolderAssetsMiddleware {
    pub uri: String,
    pub path: String,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CorsMiddleware {
    pub enable: bool,
    pub allow_origins: Option<Vec<String>>,
    pub allow_headers: Option<Vec<String>>,
    pub allow_methods: Option<Vec<String>>,
    pub max_age: Option<u64>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TimeoutRequestMiddleware {
    pub enable: bool,
    // Timeout request in milliseconds
    pub timeout: u64,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LimitPayloadMiddleware {
    pub enable: bool,
    /// Body limit. for example: 5mb
    pub body_limit: String,
}

/// Represents a generic middleware configuration that can be enabled or
/// disabled.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EnableMiddleware {
    pub enable: bool,
}

/// Represents the mailer configuration structure.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Mailer {
    pub smtp: Option<SmtpMailer>,

    #[serde(default)]
    pub stub: bool,
}

/// Represents the SMTP mailer configuration structure.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SmtpMailer {
    pub enable: bool,
    /// SMTP host. for example: localhost, smtp.gmail.com etc.
    pub host: String,
    /// SMTP port/
    pub port: u16,
    /// Enable TLS
    pub secure: bool,
    /// Auth SMTP server
    pub auth: Option<MailerAuth>,
}

/// Represents the authentication details for the mailer.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MailerAuth {
    pub user: String,
    pub password: String,
}

impl Server {
    #[must_use]
    pub fn full_url(&self) -> String {
        format!("{}:{}", self.host, self.port)
    }
}
impl Config {
    /// Creates a new configuration instance based on the specified environment.
    ///
    /// # Errors
    ///
    /// Returns [`ConfigError`] when could not convert the give path to
    /// [`Config`] struct.
    ///
    /// # Example
    ///
    /// ```rust
    /// use loco_rs::{
    ///     config::Config,
    ///     environment::Environment,
    /// };
    ///
    /// #[tokio::main]
    /// async fn load(environment: &Environment) -> Config {
    ///     Config::new(environment).expect("configuration loading")
    /// }
    pub fn new(env: &Environment) -> Result<Self> {
        let config = Self::from_folder(env, DEFAULT_FOLDER.as_path())?;
        // TODO(review): Do we really want to print all config data to the logs? it
        // might be include sensitive data such DB user password, auth secrets etc...
        info!(name: "environment_loaded", config = ?config, environment = ?env, "environment loaded");

        Ok(config)
    }

    /// Loads configuration settings from a folder for the specified
    /// environment.
    ///
    /// # Errors
    /// Returns [`ConfigError`] when could not convert the give path to
    /// [`Config`] struct.
    ///
    /// # Example
    ///
    /// ```rust
    /// use loco_rs::{
    ///     config::Config,
    ///     environment::Environment,
    /// };
    /// use std::path::PathBuf;
    ///
    /// #[tokio::main]
    /// async fn load(environment: &Environment) -> Config{
    ///     Config::from_folder(environment, &PathBuf::from("config")).expect("configuration loading")
    /// }
    pub fn from_folder(env: &Environment, path: &Path) -> Result<Self> {
        // by order of precedence
        let files = vec![
            path.join(format!("{env}.local.yaml")),
            path.join(format!("{env}.yaml")),
        ];

        let selected_path = files
            .iter()
            .find(|p| p.exists())
            .ok_or_else(|| Error::Message("no configuration file found".to_string()))?;

        info!(selected_path =? selected_path, "loading environment from");

        let content = fs::read_to_string(selected_path)?;
        let rendered = crate::tera::render_string(&content, &json!({}))?;

        serde_yaml::from_str(&rendered)
            .map_err(|err| Error::YAMLFile(err, selected_path.to_string_lossy().to_string()))
    }

    /// Get a reference to the JWT configuration.
    ///
    /// # Errors
    /// return an error when jwt token not configured
    pub fn get_jwt_config(&self) -> Result<&JWT> {
        self.auth
            .as_ref()
            .and_then(|auth| auth.jwt.as_ref())
            .map_or_else(
                || Err(Error::Any("sending email error".to_string().into())),
                Ok,
            )
    }
}
